/*
 * Copyright (C) 2012 Zhenya Leonov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jpal.io;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static jpal.io.FileWalker.VisitResult.CONTINUE;
import static jpal.io.FileWalker.VisitResult.SKIP_SIBLINGS;
import static jpal.io.FileWalker.VisitResult.TERMINATE;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.nio.charset.Charset;

import jpal.io.FileWalker.VisitResult;

import com.google.common.base.Charsets;
import com.google.common.io.ByteSink;
import com.google.common.io.ByteSource;
import com.google.common.io.CharSink;
import com.google.common.io.CharSource;
import com.google.common.io.FileWriteMode;
import com.google.common.io.Files;

/**
 * More static utility methods for working with {@link File}s.
 * 
 * @see Files
 * @see ByteSink
 * @see CharSink
 * @see CharSource
 * @see ByteSource
 * @author Zhenya Leonov
 */
final public class MoreFiles {

	private MoreFiles() {
	}

	/**
	 * Creates any necessary but nonexistent parent directories of the specified
	 * file. Note that if this operation fails it may have succeeded in creating
	 * some (but not all) of the necessary parent directories.
	 * 
	 * @param file
	 *            the specified file
	 * @return the specified file
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	public static File createParentDirs(final File file) throws IOException {
		checkNotNull(file);
		Files.createParentDirs(file);
		return file;
	}

	/**
	 * Returns a new {@code PrintStream} which writes to the given file using
	 * the specified charset.
	 * 
	 * @param file
	 *            the specified file
	 * @param charset
	 *            the character set to use when writing to the file
	 * @param autoFlush
	 *            if {@code true}, the output buffer will be flushed whenever a
	 *            byte array is written, one of the println methods is invoked,
	 *            or a newline character or byte ('\n') is written
	 * @param modes
	 *            when the {@code APPEND} mode is specified, writes will append
	 *            to the end of the file, the default mode when none is
	 *            specified is to truncate the file before writing
	 * @return new {@code PrintStream} which writes to the given file
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	public static PrintStream newPrintStream(final File file,
			final Charset charset, final boolean autoFlush,
			final FileWriteMode... modes) throws IOException {
		checkNotNull(file);
		checkNotNull(charset);
		checkNotNull(modes);
		return new PrintStream(Files.asByteSink(file, modes)
				.openBufferedStream(), autoFlush, charset.toString());
	}

	/**
	 * Returns a print writer to the given file using the specified charset.
	 * 
	 * @param file
	 *            the given file
	 * @param charset
	 *            the character set to use when writing the file
	 * @param autoFlush
	 *            if {@code true}, the {@code println}, {@code printf}, and
	 *            {@code format} methods will flush the output buffer
	 * @param modes
	 *            when the {@code APPEND} mode is specified, writes will append
	 *            to the end of the file, the default mode when none is
	 *            specified is to truncate the file before writing
	 * @throws IOException
	 *             if an I/O error occurs
	 * @return a print writer to the given file
	 * @see Charsets
	 */
	public static PrintWriter newPrintWriter(final File file,
			final Charset charset, final boolean autoFlush,
			final FileWriteMode... modes) throws IOException {
		checkNotNull(file);
		checkNotNull(charset);
		checkNotNull(modes);
		return new PrintWriter(Files.asCharSink(file, charset, modes)
				.openBufferedStream(), autoFlush);
	}

	/**
	 * Returns the specified path string normalized to use the system separator
	 * as specified by {@link File#separator}.
	 * 
	 * @param path
	 *            the specified path
	 * @return the specified path string normalized to use the system separator
	 *         as specified by {@link File#separator}
	 */
	public static String separatorsToSystem(final String path) {
		checkNotNull(path);
		if (File.separator.equals("/"))
			return path.replace("\\", "/");
		else
			return path.replace("/", "\\");
	}

	/**
	 * Walks a file tree.
	 * 
	 * This method walks a file tree rooted at a given starting file. The file
	 * tree traversal is depth-first with the given {@link FileWalker} invoked
	 * for each file encountered. File tree traversal completes when all
	 * accessible files in the tree have been visited, or a visit method returns
	 * {@link VisitResult#TERMINATE VisitResult.TERMINATE}.
	 * <p>
	 * If the file is not a directory then the
	 * {@link FileWalker#visitFile(File) visitFile(File)} is invoked. If the
	 * file is a directory, and the directory could not be opened, then the
	 * {@link FileWalker#visitFileFailed(File, IOException)
	 * visitFileFailed(File, IOException)} method is invoked with the I/O
	 * exception, after which, the file tree walk continues, by default, at the
	 * next sibling of the directory.
	 * <p>
	 * If the directory is opened successfully, then the entries in the
	 * directory, and their descendants are visited. If an entry is deleted
	 * before it is visited the
	 * {@link FileWalker#visitFileFailed(File, IOException)
	 * visitFileFailed(File, IOException)} method is invoked with a
	 * {@code FileNotFoundException}. When all entries have been visited then
	 * the {@link FileWalker#postVisitDirectory(File) postVisitDirectory(File)}
	 * method is invoked. The file tree walk then continues, by default, at the
	 * next sibling of the directory.
	 * <p>
	 * When a security manager is installed and it denies access to a file (or
	 * directory), then it is ignored and the {@code FileWalker} is not invoked
	 * for that file (or directory).
	 * <p>
	 * <b>Warning:</b> {@code java.io.File} provides no platform-independent way
	 * to detect symbolic links, as such there is no way to ensure a symbolic
	 * link to a directory is not followed when traversing a file tree. In the
	 * presence of symbolic links, you may encounter files outside the starting
	 * directory, or even end up in an infinite loop.
	 * 
	 * @param start
	 *            the starting file
	 * @param walker
	 *            the {@code FileWalker} instance to invoke for each file
	 * @throws IOException
	 *             if an I/O error occurs
	 * @throws IllegalArgumentException
	 *             if the starting file does not exist
	 * @return the starting file
	 */
	public static File walkFileTree(final File start, final FileWalker<?> walker)
			throws IOException {
		checkNotNull(start);
		checkArgument(start.exists());
		checkNotNull(walker);
		walkFileTree(start, 0, Integer.MAX_VALUE, walker);
		return start;
	}

	/**
	 * Walks a file tree.
	 * 
	 * This method walks a file tree rooted at a given starting file. The file
	 * tree traversal is depth-first with the given {@link FileWalker} invoked
	 * for each file encountered. File tree traversal completes when all
	 * accessible files in the tree have been visited, or a visit method returns
	 * {@link VisitResult#TERMINATE VisitResult.TERMINATE}.
	 * <p>
	 * If the file is not a directory, then the
	 * {@link FileWalker#visitFile(File) visitFile(File)} method is invoked. If
	 * the file is a directory, and the directory could not be opened, then the
	 * {@link FileWalker#visitFileFailed(File, IOException)
	 * visitFileFailed(File, IOException)} method is invoked with the I/O
	 * exception, after which, the file tree walk continues.
	 * <p>
	 * If the directory is opened successfully, then the entries in the
	 * directory, and their descendants are visited. If an entry is deleted
	 * before it is visited, then the
	 * {@link FileWalker#visitFileFailed(File, IOException)
	 * visitFileFailed(File, IOException)} method is invoked with a
	 * {@code FileNotFoundException}. When all entries have been visited then
	 * the {@link FileWalker#postVisitDirectory(File) postVisitDirectory(File)}
	 * method is invoked. The file tree walk then continues.
	 * <p>
	 * The {@code maxDepth} parameter is the maximum number of levels of
	 * directories to visit. A value of 0 means that only the starting file is
	 * visited, unless denied by the security manager. A value of
	 * {@link Integer#MAX_VALUE MAX_VALUE} may be used to indicate that all
	 * levels should be visited.
	 * <p>
	 * When a security manager is installed and it denies access to a file (or
	 * directory), then it is ignored and the {@code FileWalker} is not invoked
	 * for that file (or directory).
	 * <p>
	 * <b>Warning:</b> {@code java.io.File} provides no platform-independent way
	 * to detect symbolic links, as such there is no way to ensure a symbolic
	 * link to a directory is not followed when traversing a file tree. In the
	 * presence of symbolic links, you may encounter files outside the starting
	 * directory, or even end up in an infinite loop.
	 * 
	 * @param start
	 *            the starting file
	 * @param maxDepth
	 *            the maximum number of directory levels to visit
	 * @param walker
	 *            the {@code FileWalker} instance to invoke for each file
	 * @throws IOException
	 *             if an I/O error occurs
	 * @throws IllegalArgumentException
	 *             if the starting file does not exist
	 * @return the starting file
	 */
	public static File walkFileTree(final File start, final int maxDepth,
			final FileWalker<?> walker) throws IOException {
		checkNotNull(start);
		checkArgument(start.exists());
		checkNotNull(walker);
		checkArgument(maxDepth >= 0);
		walkFileTree(start, 0, maxDepth, walker);
		return start;
	}

	private static VisitResult walkFileTree(final File start, int depth,
			final int maxDepth, final FileWalker<?> walker) throws IOException {

		File[] files = null;

		try {
			if (start.isDirectory() && (files = start.listFiles()) == null)
				throw new IOException("Cannot access " + start);
			if (depth >= maxDepth || start.isFile())
				return walker.visitFile(start);
		} catch (SecurityException ex) {
			if (depth == 0)
				throw ex;
			return CONTINUE;
		} catch (IOException ex) {
			return walker.visitFileFailed(start, ex);
		}

		VisitResult result = walker.preVisitDirectory(start);

		if (result != CONTINUE)
			return result;
		for (final File file : files) {
			if (!file.exists())
				return walker.visitFileFailed(file, new FileNotFoundException(
						file.toString()));
			result = walkFileTree(file, depth + 1, maxDepth, walker);
			if (result == SKIP_SIBLINGS)
				break;
			if (result == TERMINATE)
				return TERMINATE;
		}
		return walker.postVisitDirectory(start);
	}

}